/*
 * Copyright 2011 Katsunori Koyanagi
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package sweezy.swf.decoder
{
	import flash.utils.ByteArray;
	import sweezy.swf.abc.ABC;
	import sweezy.swf.abc.Opcodes;
	import sweezy.swf.abc.opcode.UnknownOpecode;
	import sweezy.swf.abc.structure.ClassInfo;
	import sweezy.swf.abc.structure.ClassTrait;
	import sweezy.swf.abc.structure.ConstTrait;
	import sweezy.swf.abc.structure.ConstantPoolInfo;
	import sweezy.swf.abc.structure.ExceptionInfo;
	import sweezy.swf.abc.structure.FunctionTrait;
	import sweezy.swf.abc.structure.GetterTrait;
	import sweezy.swf.abc.structure.InstanceInfo;
	import sweezy.swf.abc.structure.MetadataInfo;
	import sweezy.swf.abc.structure.MetadataItemInfo;
	import sweezy.swf.abc.structure.MethodBodyInfo;
	import sweezy.swf.abc.structure.MethodInfo;
	import sweezy.swf.abc.structure.MethodTrait;
	import sweezy.swf.abc.structure.Multiname;
	import sweezy.swf.abc.structure.MultinameA;
	import sweezy.swf.abc.structure.MultinameInfo;
	import sweezy.swf.abc.structure.MultinameL;
	import sweezy.swf.abc.structure.MultinameLA;
	import sweezy.swf.abc.structure.NameL;
	import sweezy.swf.abc.structure.NameLA;
	import sweezy.swf.abc.structure.NamespaceInfo;
	import sweezy.swf.abc.structure.NamespaceSetInfo;
	import sweezy.swf.abc.structure.OptionDetail;
	import sweezy.swf.abc.structure.QNameA;
	import sweezy.swf.abc.structure.QNameInfo;
	import sweezy.swf.abc.structure.RTQName;
	import sweezy.swf.abc.structure.RTQNameA;
	import sweezy.swf.abc.structure.RTQNameL;
	import sweezy.swf.abc.structure.RTQNameLA;
	import sweezy.swf.abc.structure.ScriptInfo;
	import sweezy.swf.abc.structure.SetterTrait;
	import sweezy.swf.abc.structure.SlotTrait;
	import sweezy.swf.abc.structure.TraitInfo;
	import sweezy.swf.abc.structure.TypeName;
	import sweezy.swf.constant.InstanceInfoFlags;
	import sweezy.swf.constant.MethodInfoFlags;
	import sweezy.swf.constant.MultinameKind;
	import sweezy.swf.constant.TraitAttributes;
	import sweezy.swf.constant.TraitType;
	import sweezy.swf.io.SWFInput;

	public class ABCDecoder
	{

		private var _error:String;

		private var _in:SWFInput;

		public function ABCDecoder()
		{
		}

		public function decode(bytes:ByteArray):ABC
		{
			_error = null;

			try
			{
				return decode0(bytes);
			}
			catch (e:Error)
			{
				_error = e.message;
			}

			return null;
		}

		public function get error():String
		{
			return _error;
		}

		private function decode0(bytes:ByteArray):ABC
		{
			_in = new SWFInput(bytes);

			var abc:ABC = new ABC();
			var i:int, len:uint, data:Array;

			abc.minorVersion = _in.readUI16();
			abc.majorVersion = _in.readUI16();
			abc.constantPool = decodeConstantPoolInfo();

			data = abc.methodInfos = [];
			for (i = 0, len = _in.readEncodedU32(); i < len; i++)
			{
				data[i] = decodeMethodInfo();
			}

			data = abc.metadataInfos = [];
			for (i = 0, len = _in.readEncodedU32(); i < len; i++)
			{
				data[i] = decodeMetadataInfo();
			}

			len = _in.readEncodedU32();
			data = abc.instanceInfos = [];
			for (i = 0; i < len; i++)
			{
				data[i] = decodeInstanceInfo();
			}

			data = abc.classInfos = [];
			for (i = 0; i < len; i++)
			{
				data[i] = decodeClassInfo();
			}

			data = abc.scriptInfos = [];
			for (i = 0, len = _in.readEncodedU32(); i < len; i++)
			{
				data[i] = decodeScriptInfo();
			}

			data = abc.methodBodyInfos = [];
			for (i = 0, len = _in.readEncodedU32(); i < len; i++)
			{
				data[i] = decodeMethodBody();
			}

			_in = null;
			return abc;
		}

		private function decodeClassInfo():ClassInfo
		{
			var classInfo:ClassInfo = new ClassInfo();
			classInfo.cinit = _in.readEncodedU32();

			var data:Array = classInfo.traitInfos = [];
			for (var i:int = 0, len:uint = _in.readEncodedU32(); i < len; i++)
			{
				data[i] = decodeTraitInfo();
			}

			return classInfo;
		}

		private function decodeConstantPoolInfo():ConstantPoolInfo
		{
			var constantPool:ConstantPoolInfo = new ConstantPoolInfo();
			var i:uint, len:uint, data:Array;

			data = constantPool.integerConstants = [0];
			len = _in.readEncodedU32();
			for (i = 1; i < len; i++)
			{
				data[i] = _in.readEncodedS32();
			}

			data = constantPool.unsignedIntegerConstants = [0];
			len = _in.readEncodedU32();
			for (i = 1; i < len; i++)
			{
				data[i] = _in.readEncodedU32();
			}

			data = constantPool.doubleConstants = [NaN];
			len = _in.readEncodedU32();
			for (i = 1; i < len; i++)
			{
				data[i] = _in.readDOUBLE();
			}

			data = constantPool.stringConstants = [""];
			len = _in.readEncodedU32();
			for (i = 1; i < len; i++)
			{
				var strLen:uint = _in.readEncodedU32();
				data[i] = strLen === 0 ? "" : _in.readString(strLen);
			}

			data = constantPool.namespaceInfos = [null];
			len = _in.readEncodedU32();
			for (i = 1; i < len; i++)
			{
				data[i] = decodeNamespaceInfo();
			}

			data = constantPool.namespaceSetInfos = [null];
			len = _in.readEncodedU32();
			for (i = 1; i < len; i++)
			{
				data[i] = decodeNamespaceSetInfo();
			}

			data = constantPool.multinameInfos = [null];
			len = _in.readEncodedU32();
			for (i = 1; i < len; i++)
			{
				data[i] = decodeMultinameInfo();
			}

			return constantPool;
		}

		private function decodeInstanceInfo():InstanceInfo
		{
			var i:int, len:uint, data:Array;
			var instanceInfo:InstanceInfo = new InstanceInfo();
			instanceInfo.name = _in.readEncodedU32();
			instanceInfo.superName = _in.readEncodedU32();
			instanceInfo.flags = _in.readUI8();

			if ((instanceInfo.flags & InstanceInfoFlags.CLASS_PROTECTED_NS) === InstanceInfoFlags.CLASS_PROTECTED_NS)
			{
				instanceInfo.protectedNs = _in.readEncodedU32();
			}

			data = instanceInfo.interfaces = [];
			for (i = 0, len = _in.readEncodedU32(); i < len; i++)
			{
				data[i] = _in.readEncodedU32();
			}
			instanceInfo.iinit = _in.readEncodedU32();

			data = instanceInfo.traitInfos = [];
			for (i = 0, len = _in.readEncodedU32(); i < len; i++)
			{
				data[i] = decodeTraitInfo();
			}

			return instanceInfo;
		}

		private function decodeMetadataInfo():MetadataInfo
		{
			var metadataInfo:MetadataInfo = new MetadataInfo();
			metadataInfo.name = _in.readEncodedU32();

			var data:Array = metadataInfo.items = [];
			var len:uint = _in.readEncodedU32();
			var i:uint;
			var metadataItemInfo:MetadataItemInfo;

			for (i = 0; i < len; i++)
			{
				metadataItemInfo = new MetadataItemInfo();
				metadataItemInfo.key = _in.readEncodedU32();
				data[i] = metadataItemInfo;
			}
			for (i = 0; i < len; i++)
			{
				metadataItemInfo = data[i];
				metadataItemInfo.val = _in.readEncodedU32();
			}

			return metadataInfo;
		}

		private function decodeMethodBody():MethodBodyInfo
		{
			var data:Array, len:uint, i:int;
			var methodBodyInfo:MethodBodyInfo = new MethodBodyInfo();
			methodBodyInfo.method = _in.readEncodedU32();
			methodBodyInfo.maxStack = _in.readEncodedU32();
			methodBodyInfo.localCount = _in.readEncodedU32();
			methodBodyInfo.initScopeDepth = _in.readEncodedU32();
			methodBodyInfo.maxScopeDepth = _in.readEncodedU32();
			methodBodyInfo.opcodes = decodeOpCodes(methodBodyInfo);

			data = methodBodyInfo.exceptionInfos = [];
			for (i = 0, len = _in.readEncodedU32(); i < len; i++)
			{
				var exceptionInfo:ExceptionInfo = new ExceptionInfo();
				exceptionInfo.codeIndexFrom = _in.readEncodedU32();
				exceptionInfo.codeIndexTo = _in.readEncodedU32();
				exceptionInfo.target = _in.readEncodedU32();
				exceptionInfo.exceptionType = _in.readEncodedU32();
				exceptionInfo.varName = _in.readEncodedU32();

				data[i] = exceptionInfo;
			}

			data = methodBodyInfo.traitInfos = [];
			for (i = 0, len = _in.readEncodedU32(); i < len; i++)
			{
				data[i] = decodeTraitInfo();
			}

			return methodBodyInfo;
		}

		private function decodeMethodInfo():MethodInfo
		{
			var i:int, len:uint, data:Array;
			var methodInfo:MethodInfo = new MethodInfo();
			var parameterCount:uint = _in.readEncodedU32();

			methodInfo.returnType = _in.readEncodedU32();
			data = methodInfo.parameterTypes = [];
			for (i = 0; i < parameterCount; i++)
			{
				data[i] = _in.readEncodedU32();
			}
			methodInfo.name = _in.readEncodedU32();

			var flags:uint = methodInfo.flags = _in.readUI8();
			if ((flags & MethodInfoFlags.HAS_OPTIONAL) === MethodInfoFlags.HAS_OPTIONAL)
			{
				data = methodInfo.options = [];
				for (i = 0, len = _in.readEncodedU32(); i < len; i++)
				{
					var optionDetail:OptionDetail = new OptionDetail();
					optionDetail.val = _in.readEncodedU32();
					optionDetail.kind = _in.readUI8();
					data[i] = optionDetail;
				}
			}

			if ((flags & MethodInfoFlags.HAS_PARAM_NAMES) === MethodInfoFlags.HAS_PARAM_NAMES)
			{
				data = methodInfo.parameterNames = [];
				for (i = 0, len = parameterCount; i < len; i++)
				{
					data[i] = _in.readEncodedU32();
				}
			}

			return methodInfo;
		}

		private function decodeMultinameInfo():MultinameInfo
		{
			var kind:uint = _in.readUI8();
			var multinameInfo:MultinameInfo;

			switch (kind)
			{
				case MultinameKind.NAME_L:
					var nl:NameL = new NameL();
					multinameInfo = nl;

					break;
				case MultinameKind.NAME_L_A:
					var nla:NameLA = new NameLA();
					multinameInfo = nla;

					break;
				case MultinameKind.MULTINAME:
					var mn:Multiname = new Multiname();
					mn.name = _in.readEncodedU32();
					mn.namespaceSet = _in.readEncodedU32();
					multinameInfo = mn;

					break;
				case MultinameKind.MULTINAME_A:
					var mna:MultinameA = new MultinameA();
					mna.name = _in.readEncodedU32();
					mna.namespaceSet = _in.readEncodedU32();
					multinameInfo = mna;

					break;
				case MultinameKind.MULTINAME_L_A:
					var mnla:MultinameLA = new MultinameLA();
					mnla.namespaceSet = _in.readEncodedU32();
					multinameInfo = mnla;

					break;
				case MultinameKind.MULTINAME_L:
					var mnl:MultinameL = new MultinameL();
					mnl.namespaceSet = _in.readEncodedU32();
					multinameInfo = mnl;

					break;
				case MultinameKind.QNAME:
					var qn:QNameInfo = new QNameInfo();
					qn.ns = _in.readEncodedU32();
					qn.name = _in.readEncodedU32();
					multinameInfo = qn;

					break;
				case MultinameKind.QNAME_A:
					var qna:QNameA = new QNameA();
					qna.ns = _in.readEncodedU32();
					qna.name = _in.readEncodedU32();
					multinameInfo = qna;

					break;
				case MultinameKind.RT_QNAME:
					var rtqn:RTQName = new RTQName();
					rtqn.name = _in.readEncodedU32();
					multinameInfo = rtqn;

					break;
				case MultinameKind.RT_QNAME_A:
					var rtqna:RTQNameA = new RTQNameA();
					rtqna.name = _in.readEncodedU32();
					multinameInfo = rtqna;

					break;
				case MultinameKind.RT_QNAME_L:
					var rtqnl:RTQNameL = new RTQNameL();
					multinameInfo = rtqnl;

					break;
				case MultinameKind.RT_QNAME_L_A:
					var rtqnla:RTQNameLA = new RTQNameLA();
					multinameInfo = rtqnla;

					break;
				case MultinameKind.TYPE_NAME:
					var tn:TypeName = new TypeName();
					tn.typeName = _in.readEncodedU32();
					var data:Array = tn.types = []
					for (var i:int = 0, len:uint = _in.readEncodedU32(); i < len; i++)
					{
						data[i] = _in.readEncodedU32();
					}
					multinameInfo = tn;

					break;
			}

			return multinameInfo;
		}

		private function decodeNamespaceInfo():NamespaceInfo
		{
			var namespaceInfo:NamespaceInfo = new NamespaceInfo();
			namespaceInfo.kind = _in.readUI8();
			namespaceInfo.name = _in.readEncodedU32();

			return namespaceInfo;
		}

		private function decodeNamespaceSetInfo():NamespaceSetInfo
		{
			var namespaceSetInfo:NamespaceSetInfo = new NamespaceSetInfo();
			var len:uint = _in.readEncodedU32();
			var data:Array = namespaceSetInfo.namespaceInfos = [];
			for (var i:int = 0; i < len; i++)
			{
				data[i] = _in.readEncodedU32();
			}

			return namespaceSetInfo;
		}

		private function decodeOpCodes(body:MethodBodyInfo):Array
		{
			var len:uint = _in.readEncodedU32();
			var limit:uint = _in.position + len;
			var index:int = 0;
			var startPosition:uint = _in.position;
			var currentPosition:uint = 0;
			var opcodes:Array = [];

			body.codeLength = len;

			while (true)
			{
				currentPosition = _in.position - startPosition;
				var code:uint = _in.readUI8();
				var type:Class = Opcodes.getType(code);
				var opcode:*;
				if (type === null)
				{
					opcode = new UnknownOpecode(code);
				}
				else
				{
					opcode = new type();
				}

				opcode.position = currentPosition;
				switch (code)
				{
					case Opcodes.OP_ifeq:
					case Opcodes.OP_iffalse:
					case Opcodes.OP_ifge:
					case Opcodes.OP_ifgt:
					case Opcodes.OP_ifle:
					case Opcodes.OP_iflt:
					case Opcodes.OP_ifne:
					case Opcodes.OP_ifnge:
					case Opcodes.OP_ifngt:
					case Opcodes.OP_ifnle:
					case Opcodes.OP_ifnlt:
					case Opcodes.OP_ifstricteq:
					case Opcodes.OP_ifstrictne:
					case Opcodes.OP_iftrue:
					case Opcodes.OP_jump:
						opcode.offset = _in.readSI24();
						break;
					case Opcodes.OP_astype:
					case Opcodes.OP_coerce:
					case Opcodes.OP_debugfile:
					case Opcodes.OP_declocal:
					case Opcodes.OP_declocal_i:
					case Opcodes.OP_deleteproperty:
					case Opcodes.OP_dxns:
					case Opcodes.OP_finddef:
					case Opcodes.OP_findproperty:
					case Opcodes.OP_findpropstrict:
					case Opcodes.OP_getdescendants:
					case Opcodes.OP_getlex:
					case Opcodes.OP_getlocal:
					case Opcodes.OP_getproperty:
					case Opcodes.OP_getsuper:
					case Opcodes.OP_inclocal:
					case Opcodes.OP_inclocal_i:
					case Opcodes.OP_initproperty:
					case Opcodes.OP_istype:
					case Opcodes.OP_kill:
					case Opcodes.OP_newcatch:
					case Opcodes.OP_newclass:
					case Opcodes.OP_newfunction:
					case Opcodes.OP_pushdecimal:
					case Opcodes.OP_pushdouble:
					case Opcodes.OP_pushint:
					case Opcodes.OP_pushnamespace:
					case Opcodes.OP_pushstring:
					case Opcodes.OP_pushuint:
					case Opcodes.OP_pushuninitialized:
					case Opcodes.OP_setlocal:
					case Opcodes.OP_setproperty:
					case Opcodes.OP_setsuper:
						opcode.index = _in.readEncodedU32();
						break;
					case Opcodes.OP_getscopeobject:
						opcode.index = _in.readUI8();
						break;
					case Opcodes.OP_getglobalslot:
					case Opcodes.OP_getslot:
					case Opcodes.OP_setglobalslot:
					case Opcodes.OP_setslot:
						opcode.slotIndex = _in.readEncodedU32();
						break;
					case Opcodes.OP_callstatic:
					case Opcodes.OP_callmethod:
					case Opcodes.OP_callproperty:
					case Opcodes.OP_callproplex:
					case Opcodes.OP_callsuper:
					case Opcodes.OP_callsupervoid:
					case Opcodes.OP_callpropvoid:
					case Opcodes.OP_constructprop:
						opcode.index = _in.readEncodedU32();
						opcode.argCount = _in.readEncodedU32();
						break;
					case Opcodes.OP_applytype:
					case Opcodes.OP_call:
					case Opcodes.OP_construct:
					case Opcodes.OP_constructsuper:
					case Opcodes.OP_newarray:
					case Opcodes.OP_newobject:
						opcode.argCount = _in.readEncodedU32();
						break;
					case Opcodes.OP_add_p:
					case Opcodes.OP_convert_m_p:
					case Opcodes.OP_decrement_p:
					case Opcodes.OP_divide_p:
					case Opcodes.OP_increment_p:
					case Opcodes.OP_modulo_p:
					case Opcodes.OP_multiply_p:
					case Opcodes.OP_negate_p:
					case Opcodes.OP_subtract_p:
						opcode.param = _in.readEncodedU32();
						break;
					case Opcodes.OP_declocal_p:
					case Opcodes.OP_inclocal_p:
						opcode.param = _in.readEncodedU32();
						opcode.index = _in.readEncodedU32();
						break;
					case Opcodes.OP_pushshort:
						opcode.shortValue = _in.readEncodedU32();
						break;
					case Opcodes.OP_pushbyte:
						opcode.byteValue = _in.readUI8();
						break;
					case Opcodes.OP_debugline:
						opcode.lineNumber = _in.readEncodedU32();
						break;
					case Opcodes.OP_debug:
						opcode.debugType = _in.readUI8();
						opcode.index = _in.readEncodedU32();
						opcode.register = _in.readUI8();
						opcode.extra = _in.readEncodedU32();
						break;
					case Opcodes.OP_hasnext2:
						opcode.objectRegister = _in.readEncodedU32();
						opcode.indexRegister = _in.readEncodedU32();
						break;
					case Opcodes.OP_lookupswitch:
						opcode.defaultOffset = _in.readSI24();
						var data:Array = opcode.caseOffsets = [];
						for (var i:int = 0, l:uint = _in.readEncodedU32() + 1; i < l; i++)
						{
							data[i] = _in.readSI24();
						}
						break;
				}

				opcode.size = _in.position - opcode.position - startPosition;
				opcodes[index++] = opcode;
				if (_in.position >= limit)
				{
					break;
				}
			}

			return opcodes;
		}

		private function decodeScriptInfo():ScriptInfo
		{
			var scriptInfo:ScriptInfo = new ScriptInfo();
			scriptInfo.init = _in.readEncodedU32();

			var data:Array = scriptInfo.traitInfos = [];
			for (var i:int = 0, len:uint = _in.readEncodedU32(); i < len; i++)
			{
				data[i] = decodeTraitInfo();
			}

			return scriptInfo;
		}

		private function decodeTraitInfo():TraitInfo
		{
			var traitInfo:TraitInfo;
			var name:uint = _in.readEncodedU32();
			var attributes:uint = _in.readUB(4);
			var traitType:uint = _in.readUB(4);

			switch (traitType)
			{
				case TraitType.CLASS:
					var classTrait:ClassTrait = new ClassTrait();
					classTrait.slotId = _in.readEncodedU32();
					classTrait.classIndex = _in.readEncodedU32();
					traitInfo = classTrait;

					break;
				case TraitType.CONST:
					var constTrait:ConstTrait = new ConstTrait();
					constTrait.slotId = _in.readEncodedU32();
					constTrait.typeName = _in.readEncodedU32();
					constTrait.vIndex = _in.readEncodedU32();
					if (constTrait.vIndex > 0)
					{
						constTrait.vKind = _in.readUI8();
					}
					traitInfo = constTrait;

					break;
				case TraitType.SLOT:
					var slotTrait:SlotTrait = new SlotTrait();
					slotTrait.slotId = _in.readEncodedU32();
					slotTrait.typeName = _in.readEncodedU32();
					slotTrait.vIndex = _in.readEncodedU32();
					if (slotTrait.vIndex > 0)
					{
						slotTrait.vKind = _in.readUI8();
					}
					traitInfo = slotTrait;

					break;
				case TraitType.FUNCTION:
					var functionTrait:FunctionTrait = new FunctionTrait();
					functionTrait.slotId = _in.readEncodedU32();
					functionTrait.functionIndex = _in.readEncodedU32();
					traitInfo = functionTrait;

					break;
				case TraitType.METHOD:
					var methodTrait:MethodTrait = new MethodTrait();
					methodTrait.dispId = _in.readEncodedU32();
					methodTrait.method = _in.readEncodedU32();
					traitInfo = methodTrait;

					break;
				case TraitType.GETTER:
					var getterTrait:GetterTrait = new GetterTrait();
					getterTrait.dispId = _in.readEncodedU32();
					getterTrait.method = _in.readEncodedU32();
					traitInfo = getterTrait;

					break;
				case TraitType.SETTER:
					var setterTrait:SetterTrait = new SetterTrait();
					setterTrait.dispId = _in.readEncodedU32();
					setterTrait.method = _in.readEncodedU32();
					traitInfo = setterTrait;

					break;
			}

			traitInfo.name = name;
			traitInfo.attributes = attributes;

			if ((attributes & TraitAttributes.METADATA) === TraitAttributes.METADATA)
			{
				var data:Array = traitInfo.metadata = [];
				for (var i:int = 0, len:uint = _in.readEncodedU32(); i < len; i++)
				{
					data[i] = _in.readEncodedU32();
				}
			}

			return traitInfo;
		}
	}
}
